/*=============================================================================
    jibits/geom.js
    Copyright (C) 2008-2009, Donald W. Griffin
    All rights reserved.
    [MIT license :: see license.txt for details]
=============================================================================*/
$module("jibits.geom");

$namespace ("jibits", {

Point : $class(
{
    ctor : function (x, y)
    {
        $super(arguments, this);

        this.set.apply(this, arguments);
    },

    X  : function () { return this.x; },
    Y  : function () { return this.y; },

    clone : function ()
    {
        return new jibits.Point(this.x, this.y);
    },

    offset : function (dx, dy)
    {
        this.x += dx;
        this.y += (arguments.length == 1) ? dx : dy;
        return this;
    },

    set : function (x, y)
    {
        if (arguments.length < 2)
            this.x = this.y = x || 0;
        else
        {
            this.x = x || 0;
            this.y = y || 0;
        }

        return this;
    },

    stringify : function ()
    {
        return "{x="+this.x+",y="+this.y+"}";
    }
}),

Size : $class(
{
    ctor : function (w, h)
    {
        $super(arguments, this);

        this.set.apply(this, arguments);
    },

    H  : function () { return this.h; },
    W  : function () { return this.w; },

    clone : function ()
    {
        return new jibits.Size(this.w, this.h);
    },

    grow : function (dw, dh)
    {
        this.w += dw;
        this.h += (arguments.length == 1) ? dw : dh;
        return this;
    },

    set : function (w, h)
    {
        if (arguments.length < 2)
            this.w = this.h = w || 0;
        else
        {
            this.w = w || 0;
            this.h = h || 0;
        }
        return this;
    },

    stringify : function ()
    {
        return "{w="+this.w+",h="+this.h+"}";
    }
}),

Span : $class(
{
    ctor : function (b, e)
    {
        $super(arguments, this);

        this.set.apply(this, arguments);
    },

    contains : function (x)
    {
        return this.beg <= x && x < this.end;
    },

    clone : function ()
    {
        return new jibits.Span(this.beg, this.end);
    },

    middle : function ()
    {
        return (this.beg + this.end) / 2;
    },

    moveTo : function (v)
    {
        return this.set(v, v + this.size());
    },

    offset : function (by)
    {
        this.beg += by;
        this.end += by;
        return this;
    },

    set : function (b, e)
    {
        if (arguments.length < 2)
            this.beg = this.end = b || 0;
        else
        {
            this.beg = b || 0;
            this.end = e || 0;
        }
        return this;
    },

    size : function ()
    {
        return this.end - this.beg;
    },

    stringify : function ()
    {
        return "{b="+this.beg+",e="+this.end+"}";
    }
}),

Rect : $class(
{
    ctor : function (x, y, w, h)
    {
        $super(arguments, this);

        this.set.apply(this, arguments);
    },

    clone : function ()
    {
        return new jibits.Rect(this.horz, this.vert);
    },

    contains : function (x, y)
    {
        if (arguments.length == 1) // if (contains(pt) usage)
        { y = x.y; x = x.x; }

        return this.horz.contains(x) && this.vert.contains(y);
    },

    moveTo : function (x, y)
    {
        this.horz.moveTo(x);
        this.vert.moveTo(y);
        return this;
    },

    offset : function (x, y)
    {
        if (arguments.length == 1) // if (offset(pt) usage)
        { y = x.y; x = x.x; }

        this.horz.offset(x);
        this.vert.offset(y);
        return this;
    },

    set : function (x, y, w, h)
    {
        if (arguments.length == 4) // if (xywh)
        {
            x = x || 0; y = y || 0; w = w || 0; h = h || 0;
            this.horz = new jibits.Span(x, x + w);
            this.vert = new jibits.Span(y, y + h);
        }
        else if (arguments.length == 2 && "x" in y) // if (pt/pt)
        {
            this.horz = new jibits.Span(x.x, y.x);
            this.vert = new jibits.Span(x.y, y.y);
        }
        else if (arguments.length == 2 && "w" in y) // if (pt/sz)
        {
            this.horz = new jibits.Span(x.x, x.x + y.w);
            this.vert = new jibits.Span(x.y, x.y + y.h);
        }
        else if (arguments.length == 2 && "beg" in x) // if (hspan/vspan)
        {
            this.horz = x.clone();
            this.vert = y.clone();
        }
        else if (arguments.length == 1) // if (rect)
        {
            this.horz = x.horz.clone();
            this.vert = x.vert.clone();
        }
        else
        {
            this.horz = new jibits.Span();
            this.vert = new jibits.Span();
        }

        return this;
    },

    size : function ()
    {
        return new jibits.Size(this.horz.size(), this.vert.size());
    },

    stringify : function ()
    {
        return "{x="+this.X()+",y="+this.Y()+",w="+this.W()+",h="+this.H()+"}";
    },

    TL : function () { return new jibits.Point(this.X(), this.Y()); },
    TR : function () { return new jibits.Point(this.R(), this.Y()); },
    BL : function () { return new jibits.Point(this.X(), this.B()); },
    BR : function () { return new jibits.Point(this.R(), this.B()); },

    X : function () { return this.horz.beg; },
    Y : function () { return this.vert.beg; },
    H : function () { return this.vert.size(); },
    W : function () { return this.horz.size(); },

    L : function () { return this.horz.beg; },
    T : function () { return this.vert.beg; },
    R : function () { return this.horz.end; },
    B : function () { return this.vert.end; }
}),

/**
This class is used internally to apply placement values to one axis at a time.
The placement value is specified as a percentage and an optional offset. Both
can be positive or negative. The syntax is:

        [+/-]#[.#]%[+/-#]

For example, "50%+2". These values are used to place spans at some specified
coordinate. The "50%+2" placement would indicate that the span's 50% point is
the point to place on a coordinate, but offset by 2.
*/
Placement1D : $class(function ()
{
    var regex = /^([+-]?\d+(?:[.]\d*)?)[%]([+-]?\d+)?$/;

    return (
    {
        /**
        Accepts the placement in string form. This string looks like "100%+1"
        for "100% plus 1", or "99%-1" for "99% minus one".
        */
        ctor : function (pl)
        {
            var m = regex.exec(pl);
            $assert(m, "Invalid linear placement: '", pl, "'");

            this.delta = m[2] ? parseInt(m[2]) : 0;
            this.scale = parseFloat(m[1]) / 100.0;
        },

        /**
        Calculates the anchor point of the given span. The anchor is at this
        placement's percentage of the span plus the delta. For example:

            var sp = new jibits.Span(10, 110); // range is [10, 110)
            var v = new jibits.Placement1D("75%+2").anchor(sp);

            // v = 10 + (0.75 * 100) + 2 = 87
        */
        anchor : function (span)
        {
            var v = span.size();

            v = parseInt(Math.floor(v * this.scale)) + this.delta;
            v += span.beg;

            return v;
        },

        /**
        Places the given span's anchor point at the specified coordinate. The
        delta is calculated using the anchor method and is applied to the span
        as an offset. The span is returned.

        @param span The span to place.
        @param c The coordinate on which to place the span.
        @return The supplied (and now offset) span object.
        */
        place : function (span, c)
        {
            var delta = c - this.anchor(span);

            span.offset(delta);
            return span;
        }
    });
}()),

/**
This class holds two Placement1D objects to form a 2D placement. The components
can be specified as a "horz,vert" comma separated pair such as "100%+1,99%-2".
There are two uses for placements:

  * To specify an anchor point in or on a rect.
  * To specify how a rect is placed with respect to an anchor point.

In both cases, the it very common that one of the placement components to be
either 0% or 100% which indicates the edges of the rectangle. This is not a
requirement, but is typical. Another common value is 50% to effect centering.

To see how anchor points are determined, consider these examples:

    var pt = new jibits.Placement("0%,0%").anchor($rect(10, 20, 100, 200));

    // pt = { x:10, y:20 }

    pt = new jibits.Placement("50%,100%-1").anchor($rect(10, 20, 100, 200));

    // pt.x = 10 + (0.50 * 100) + 0 = 60
    // pt.y = 20 + (1.00 * 200) - 1 = 219

Similar to anchor, the place method places a rect such that its anchor point is
at a specified location.

For example:

    var r = new jibits.Placement("0%,0%").place($rect(10, 20, 100, 200),
                                                $point(500, 700));

    // anchor is (10, 20) which now moves to (500, 700):
    // r = { x:500, y:700, w:100, h:200 }  (LT is on pt)

    r = new jibits.Placement("50%,0%").place($rect(10, 20, 100, 200),
                                             $point(500, 700));

    // anchor is (60, 20) which now moves to (500, 700):
    // r = { x:450, y:700, w:100, h:200 }  (hcenter/top on pt)
*/
Placement : $class(
{
    /**
    Accepts two Placement1D's (as string or object) or a comma-separated pair.
    */
    ctor : function (h, v)
    {
        if (arguments.length == 1)
        {
            var parts = h.split(",");
            $assert(parts.length == 2, "Invalid placement: '", h, "'");

            h = parts[0];
            v = parts[1];
        }

        this.horz = (h instanceof jibits.Placement1D)
                        ? h : new jibits.Placement1D(h);
        this.vert = (v instanceof jibits.Placement1D)
                        ? v : new jibits.Placement1D(v);
    },

    from : $static(function (pl)
    {
        if (typeof(pl) == "string")
            return new jibits.Placement(pl);
        return pl;
    }),

    /**
    Calculates the anchor point of the given rectangle. The anchor is at this
    placement's percentage plus the delta on each span. For example:

        var r = $rect(10, 20, 100, 200); // xywh
        var a = new jibits.Placement("75%+2,100%-1").anchor(r);

        // a.x = 10 + (0.75 * 100) + 2 = 87
        // a.y = 20 + (1.00 * 200) - 1 = 219
    */
    anchor : function (rect)
    {
        var x = this.horz.anchor(rect.horz);
        var y = this.vert.anchor(rect.vert);
        return $point(x, y);
    },

    /**
    Places the given rect at the specified point (pt). This object determines
    the placement of the rect with respect to the point.
    */
    place : function (rect, pt)
    {
        this.horz.place(rect.horz, pt.x);
        this.vert.place(rect.vert, pt.y);
        return rect;
    }
}),

/**
Describes how an element is placed or positioned. Elements are positioned using
one horizontal (either "left" or "right") and one vertical (either "top" or
"bottom") coordinate. This detail should come by way of the CSS rules for an
element, but it is not clear how to properly ask the question.
*/
Positioning : $enum([ "TL", "TR", "BL", "BR" ],
{
    ctor : function ()
    {
        $super(arguments, this); // pass all args

        this.altVert = (this.name.charAt(0) != "T");
        this.altHorz = (this.name.charAt(1) != "L");
        this.vert = this.altVert ? "bottom" : "top";
        this.horz = this.altHorz ? "right"  : "left";
    },

    pack : function (rect)
    {
        var ret = { }, x = rect.X(), y = rect.Y();
        ret[this.horz] = x;
        ret[this.vert] = y;
        return ret;
    },

    /**
    Places the given rect at the specified point (pt). This object determines
    how the rect is placed (left/right, top/bottom) and the placement (pl) is
    used to determine the placement of the rect with respect to the point.
    */
    place : function (rect, pt, pl)
    {
        if (typeof(pl) == "string")
            pl = new jibits.Placement(pl);

        var r = pl.place(rect.clone(), pt);
        return this.pack(r);
    },

    /**
    Translates the augmented point to a point for this positioning. Such points
    are returned by getTotalOffset and getPosOffset.
    */
    xlatPt : function (pt)
    {
        return $point(this.altHorz ? pt.alt.x : rect.x,
                      this.altVert ? pt.alt.y : rect.y);
    },

    /**
    Translates the augmented rect to a rect for this positioning. Such rects
    are returned by getTotalOffsetRect and getPosOffsetRect.
    */
    xlatRect : function (rect)
    {
        return $rect(this.altHorz ? rect.alt.horz : rect.horz,
                     this.altVert ? rect.alt.vert : rect.vert);
    }
})

}); // namespace jibits

// aliases
zjs.copyProps(jibits.Positioning,
{
    LT : jibits.Positioning.TL,
    RT : jibits.Positioning.TR,
    LB : jibits.Positioning.BL,
    RB : jibits.Positioning.BR
});

function $point (x, y)
{
    return $new(jibits.Point, arguments);
}

function $rect (x, y, w, h)
{
    return $new(jibits.Rect, arguments);
}

function $size (w, h)
{
    return $new(jibits.Size, arguments);
}
